package com.hup.utils.commons.io;

import com.hup.utils.commons.exception.AppCommonException;
import com.hup.utils.commons.exception.CauseException;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.springframework.util.DigestUtils;
import sun.awt.shell.ShellFolder;

import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileSystemView;
import java.awt.Image;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Objects;

/**
 * @author hugan
 * @date 2019/8/17
 */
@Log4j2
public class FileUtil {
    private static final char[] IllegalFileNameChar = {
            '\\', '/', ':',
            '*', '?', '"',
            '<', '>', '|',
    };

    /**
     * 检查[用户输入的文件名]是否有非法字符[\/: *?" <>|]
     *
     * @param fileName 仅文件名,可以包括扩展名; 不建议在完成路径拼接后使用 {@link File#getName()}作为入参,因为会拦截不了[\/]
     * @apiNote 大部分非法字符会在文件写入时就抛异常, 但部分字符会写入成功如[: \]
     */
    public static void assertFileName(String fileName) {
        for (char c : IllegalFileNameChar) {
            if (fileName.indexOf(c) != -1) {
                throw new IllegalArgumentException("文件名错误:" + fileName);
            }
        }
    }

    /**
     * 将文件名中的非法路径字符移除掉
     *
     * @param fileName 仅文件名,可以含扩展名
     */
    public static String getSafeFileName(String fileName) {
        ArrayList<Integer> lstIllegalIndex = new ArrayList<>();//非法字符的index,倒序的
        for (int i = fileName.length() - 1; i >= 0; i--) {
            char c = fileName.charAt(i);
            for (char illegal : IllegalFileNameChar) if (illegal == c) lstIllegalIndex.add(i);
        }
        if (lstIllegalIndex.isEmpty()) {
            return fileName;
        } else {
            //倒序的删除对应的char
            StringBuilder sb = new StringBuilder(fileName);
            for (Integer index : lstIllegalIndex) sb.deleteCharAt(index);
            return sb.toString();
        }
    }

    /**
     * @param ext 小写字母,含'.'
     */
    public static boolean extOf(File file, String ext) {
        return file.getAbsolutePath().toLowerCase().endsWith(ext);
    }

    /**
     * @param exts 小写字母,含'.'
     */
    public static boolean extOf(File file, String[] exts) {
        String name = file.getName().toLowerCase();
        for (String ext : exts) {
            if (name.endsWith(ext)) return true;
        }
        return false;
    }

    /**
     * @apiNote 改变文件名不会导致md5变化;只要文件内容修改过,都会不一样
     */
    public static String getMD5(String fileName) {
        try (FileInputStream pis = new FileInputStream(fileName)) {
            return DigestUtils.md5DigestAsHex(pis);
        } catch (IOException e) {
            throw new CauseException("获取MD5失败:", e);
        }
    }

    /**
     * 比较文件内容和输入流内容是否一致;
     * 如果文件不存在或内容不一致,则把输入流内容覆盖到文件
     *
     * @param inputStream 任何返回情况下,输入流都会被close!
     * @return 0:文件一致;1:文件不存在,写入文件;-1:文件存在但于目标不一致,覆盖文件
     */
    public static int writeIfNotSame(File file, InputStream inputStream) throws IOException {
        if (!file.exists()) {
            FileUtils.copyInputStreamToFile(inputStream, file);//将stream覆盖到file,并关闭stream!,下同
            return 1;
        }

        /*
        在MD5不一致时,DigestUtils和FileUtils都要读取inputStream;
        所以需要将inputStream提取出来;
        inputStream.read(data),对一般InputStream没问题,但在读取resources的dll时(JarURLInputStream)会有bug
         */
        ByteArrayOutputStream output = new ByteArrayOutputStream(2048);
        output.write(inputStream);
        byte[] temp = output.toByteArray();

        log.debug("len={}", temp.length);
        try (FileInputStream pis = new FileInputStream(file)) {
            String md5File = DigestUtils.md5DigestAsHex(pis);
            String md5Stream = DigestUtils.md5DigestAsHex(temp);
            if (!Objects.equals(md5File, md5Stream)) {
                log.debug("md5File={}, md5Stream={}", md5File, md5Stream);
                FileUtils.writeByteArrayToFile(file, temp, false);
                IOUtils.closeQuietly(inputStream);
                return -1;
            } else {
                IOUtils.closeQuietly(inputStream);
                return 0;
            }
        }
    }

    /**
     * @param child nullable,如果不为空,则返回临时文件夹下的child,同样以'分隔符'结尾
     * @return 系统临时文件夹, 以'文件分隔符'结尾的路径
     */
    public static String getTempPath(String child) {
        String temp = System.getProperty("java.io.tmpdir");
        StringBuilder sb = new StringBuilder(temp);
        if (!temp.endsWith(File.separator)) {
            sb.append(File.separator);
        }
        if (child != null) {
            sb.append(child);
            if (!child.endsWith(File.separator)) {
                sb.append(File.separator);
            }
        }
        return sb.toString();
    }

    public static void createDir(String... paths) {
        for (String path : paths) {
            File file = new File(path);
            if (!file.exists() && !file.mkdirs()) {
                throw new AppCommonException("目录创建异常:" + file.getAbsolutePath());
            }
        }
    }

    /**
     * 获取文件数据对象,用于
     * {@link ShellFolder#getLinkLocation()}, {@link ShellFolder#getIcon(boolean)}
     */
    public static ShellFolder getShellFolder(File f) throws FileNotFoundException {
        try {
            Method method = FileSystemView.class.getDeclaredMethod("getShellFolder", File.class);
            method.setAccessible(true);
            return (ShellFolder) method.invoke(FileSystemView.getFileSystemView(), f);
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            //getShellFolder的异常是没有填入文件路径的
            if (cause instanceof FileNotFoundException) throw new FileNotFoundException(f.getPath());
            throw new CauseException("文件解析异常:", e);
        } catch (ReflectiveOperationException e) {
            throw new CauseException("文件解析异常:", e);
        }
    }

    /**
     * 获取快捷方式的源文件对象;
     * 避免sun.awt.shell.Win32ShellFolder2.getLinkLocation()在快捷方式源文件丢失时,弹出"是否删除"确认框
     *
     * @param sf 符合 {@link ShellFolder#isLink()}==true的文件
     * @apiNote 注意:如果快捷方式源文件丢失,则本方法返回的sf的getIcon会返回null
     */
    public static ShellFolder getLinkLocation(ShellFolder sf) throws FileNotFoundException {
        try {
            //调用private方法:sun.awt.shell.Win32ShellFolder2.getLinkLocation(boolean)
            Method method = sf.getClass().getDeclaredMethod("getLinkLocation", boolean.class);
            method.setAccessible(true);
            return (ShellFolder) method.invoke(sf, false);
        } catch (Exception e) {
            return sf.getLinkLocation();
        }
    }

    /**
     * 获取快捷方式的真实路径
     *
     * @return 文件路径:非快捷方式(.lnk)时返回原文件路径
     */
    public static String getLinkAbsolutePath(File file) throws FileNotFoundException {
        if (!file.exists()) throw new FileNotFoundException(file.getPath());
        ShellFolder sf = getShellFolder(file);
        if (sf.isLink()) {
            return getLinkLocation(sf).getAbsolutePath();
        } else {
            return file.getAbsolutePath();
        }
    }

    /**
     * 获取文件图标;
     * .图像类型:系统的[图片类型图标],不是原文件图像
     * .快捷方式,其他文件:与系统的图标一致
     *
     * @param isLarge true-大图标
     * @return 屏幕缩放比例不是100%时,图标的左边和下边都是缺失的!jdk问的bug, {@link JFileChooser}界面的图标都是这样
     * @apiNote 如果是lnk:源文件不存在时,也能正确得到图标(图片类型图标)
     */
    public static ImageIcon getFileIcon(File file, boolean isLarge) throws FileNotFoundException {
        if (!file.exists()) throw new FileNotFoundException(file.getPath());
        if (!isLarge) {
            return (ImageIcon) FileSystemView.getFileSystemView().getSystemIcon(file);
        } else {
            ShellFolder sf = getShellFolder(file);
            Image largeIcon = sf.getIcon(true);
            return new ImageIcon(largeIcon, sf.getFolderType());
        }
    }

    /**
     * 标准化路径分隔符;与java.io.WinNTFileSystem#normalize(java.lang.String)类似??
     *
     * @param path 文件路径,notNull
     */
    public static String normalize(String path) {
        String separator = File.separator;
        String other = (separator.equals("\\")) ? "/" : "\\";
        if (!path.contains(other)) return path;
        return path.replace(other.charAt(0), separator.charAt(0));
    }

}
